package com.sakila.configuration;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.servlet.configuration.EnableWebMvcSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import com.sakila.jaas.MemberAuthentication;
import com.sakila.jaas.RoleDefinition;
import com.sakila.jaas.StaffAuthentication;
import com.sakila.service.MemberService;
import com.sakila.service.StaffService;

@Configuration
@EnableWebMvcSecurity
public class SecurityConfig{
	@Configuration
	@Order(1)
	public static class StaffSecurity extends WebSecurityConfigurerAdapter{
		@Autowired
		private StaffService staffService;
		@Autowired
		private DataSource dataSource;
		
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/staff/**")
				.authorizeRequests()
				.antMatchers("/staff/admin/**").hasAuthority(RoleDefinition.ROLE_MANAGER)						
				.antMatchers("/staff/**").hasAuthority(RoleDefinition.ROLE_STAFF)
				.antMatchers("/staff/**").fullyAuthenticated()	
				.anyRequest().authenticated()
				.and()											//trim filter will transfer empty string to null
				.formLogin().loginPage("/staff/login").failureUrl("/staff/login?error=error").defaultSuccessUrl("/staff/index").permitAll()
				.and()																	   //12hours for staff
				.rememberMe().tokenRepository(persistentTokenRepository()).tokenValiditySeconds(43_200)
				.and()
				.logout().logoutUrl("/staff/logout").logoutSuccessUrl("/staff")/*.permitAll()*/
				.and().
				exceptionHandling().accessDeniedPage("/staff/login?error=deny");
		}
		
		protected void configure(AuthenticationManagerBuilder auth)
				throws Exception {
			auth.
				userDetailsService(new StaffAuthentication(staffService))
				.passwordEncoder(passwordEncoder());
		}
		
		/**
		 * auto write data to the database table 'persistent_logins'
		 * here's the creating sql, just copy to use(reference:{@link http://docs.spring.io/autorepo/docs/spring-security/3.2.8.RELEASE/reference/htmlsingle/#remember-me-persistent-token}):
		 * 				create table persistent_logins (username varchar(64) not null,
		 * 	                 							series varchar(64) primary key,
		 * 	                   							token varchar(64) not null,
		 * 	                         					last_used timestamp not null)
		 * 
		 */
		@Bean
		public PersistentTokenRepository persistentTokenRepository() {
			JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
			db.setDataSource(dataSource);
			return db;
		}
		
		@Bean
		public PasswordEncoder passwordEncoder(){
			return new BCryptPasswordEncoder(16);
		}
	}
	
	@Configuration
	@Order(10)
	public static class MemberSecurity extends WebSecurityConfigurerAdapter{
		@Autowired
		private MemberService memberService;
		@Autowired
		private DataSource dataSource;
		
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/member/**")
				.authorizeRequests()
				.antMatchers("/member/", "/member/**").fullyAuthenticated()
				.antMatchers("/member/**").hasAuthority(RoleDefinition.ROLE_MEMBER)
				.and()											//trim filter will transfer empty string to null
				.formLogin().loginPage("/member/login").failureUrl("/member/login?error=error").defaultSuccessUrl("/member/").permitAll()
				.and()																		//a month
				.rememberMe().tokenRepository(persistentTokenRepository()).tokenValiditySeconds(2_592_000)
				.and()
				.logout().logoutUrl("/member/logout").logoutSuccessUrl("/")
				.and().
				exceptionHandling().accessDeniedPage("/member/login?error=deny");;
		}
		
		protected void configure(AuthenticationManagerBuilder auth)
				throws Exception {
			auth.
				userDetailsService(new MemberAuthentication(memberService))
				.passwordEncoder(passwordEncoder());
		}
		
		@Bean
		public PersistentTokenRepository persistentTokenRepository() {
			JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
			db.setDataSource(dataSource);
			return db;
		}
		
		/**
		 *  Q:each time a different hash value is generated for the same password, 
		 *  then how can one password can be compared to one of its generated hash value?
		 *  A:BCrypt algorithm works for you.
		 */
		@Bean
		public PasswordEncoder passwordEncoder(){
			return new BCryptPasswordEncoder(16);
		}
	}
	
	@Configuration
	public static class RootPathSecurity extends WebSecurityConfigurerAdapter{	
		protected void configure(HttpSecurity http) throws Exception {
			http
				.antMatcher("/*")
				.authorizeRequests()
				.antMatchers("/goto_register", "/register").access("hasRole('ROLE_ANONYMOUS') and not hasRole('ROLE_MEMBER')")
				.anyRequest().permitAll();
		}
	}
}
